華麗的特效很炫沒錯,但往往伴隨著效能卡頓的問題 QQ。在 網頁完整渲染的過程,會經過一系列的步驟,直到讓使用者看見畫面,光是改變元素大小,就會使流程在走一遍,這對瀏覽器是非常複雜的。
有些 CSS 屬性改變會觸發一整個過程,尤其是改變物理上的距離、大小 (margin 、 width 等等),不過並不是所有的過程都必須經歷,在優化上可以盡量選擇影響幅度小的屬性,例如 : transform 就是一個好選擇。
舉例 文字畫線的動畫 就可以好幾種方法表示 :
這幾種牽涉網頁渲染路徑各有不同 ,會發現 width
跟 right
多走了一個步驟
在 motion API 有提供 layer
props ,把會使瀏覽器操作 layout 的部份進行優化,改成較少效能消耗的 transform,至於怎麼做呢 ? 初步了解是這樣的 :
這裡只講到大概的原理,實際上應該會有產生正確位置的元素,並且在最後做交換。
有興趣可以參考 Matt Perry (framer motion 的維護者之一) 在 2020 Next.js conf 上 簡單解釋 layout props 做哪些事,以及他在 新的 motion library 上更深入的解釋 。
layout
propslayout
props 應用時機 ?在前幾篇的文章中都使用 animate
或 initial
達到基本動畫操作,這些操作有一些共通點,他們不會 reflow
,像是 x
、y
位移是對應 transform ,基本上對效能來說負擔不大,因為都只走 paint
這條路。
在 官方範例 舉 flex 改變 justify-content 為例, 其屬性有 : flex-start
、 center
與 flex-end
,那為什麼位移不能直接 flex-start
安排到 flex-end
呢 ? 這樣也省去 x 計算值的問題。
// 官方範例 : https://codesandbox.io/s/framer-motion-2-layout-animations-kij8p?from-embed
.switch {
display: flex;
justify-content: flex-start;
//... 略
}
// 下面會看到 jsx 怎麼被加入 data-
.switch[data-isOn="true"] {
justify-content: flex-end;
}
// 官方應用 data- 的方式改變,當 isOn state 改變就改變 justify-content 的值
<div className="switch" data-isOn={isOn} onClick={toggleSwitch}>
{/* 添加了 layout */}
<motion.div className="handle" layout transition={spring} />
</div>
先來看看 CSS trigger 對改變 justify-content
所觸發的渲染包含那些階段 :
從上圖可知道會影響到其他元素的位置,進行重排 (reflow) 的操作,來看一下實際上的差別。
layout
props 會發現明顯的截斷,有 layout
props 動畫會保持絲滑的運行。Magic !
(圖源自網路)
官方列舉出使用 layout
props 在哪些方面 :
目前已知的問題是 box-shadow
與 border-radius
會在 transform 轉化過程中出現扭曲 (distort) 現象,為了解決這個問題,官方建議把他設為動畫值,motion 會自動矯正扭曲的問題,不過還是有一定的限制 :
border-radius
只能是用 px
或 %
。box-shadow
: 只有單個 box-shadow 的情況。如果不設成動畫值,就使用 還我漂漂拳,打回 style 裡面 :
<motion.div layout style={{ borderRadius: 20 }} />
可以看這篇 非常清楚的範例文章
// CSS
.box {
width: 20px;
height: 20px;
border-radius: 20px; // 出現點比較早,因此在動畫操作發生扭曲
}
.box[data-expanded="true"] {
width: 150px;
height: 150px;
}
// JS
<motion.div
layout
className="box"
data-expanded={expanded}
/>
明顯的漸變斷層
// CSS
.box {
width: 20px;
height: 20px;
}
.box[data-expanded="true"] {
width: 150px;
height: 150px;
}
// JS
<motion.div
layout
className="box"
data-expanded={expanded}
style={{
borderRadius: '20px' // 還原在這裡使用
}}
/>
變得更滑順多了
我把 duration 調長,以至於可以看清楚轉化過程到底發生什麼問題 :
由於我使用 box-shadow
的 inset 做出月亮的缺口,導致會有個順間閃爍。
原始程式碼 :
<motion.div
className="panBox"
animate={{
background: theme ? "#ABD9FF" : "#182747",
}}
>
<motion.span
className="panThumb"
animate={{
background: theme ? "#fa0" : "#182747",
// 位移是使用 x 而不是 flex 的排版
x: theme ? 0 : "calc(100px - 40px)",
rotate: theme ? 0 : -160,
// 罪魁禍首在這裡
boxShadow: theme
? "inset 0px 0px rgb(0, 0, 0,0)"
: "inset 15px 8px #fa0",
}}
transition={{
duration: 1,
}}
onPan={(e, info) => {
if (info.offset.x < 0) {
setTheme(true);
}
if (info.offset.x > 0) {
setTheme(false);
}
}}
/>
</motion.div>
接著補上我們的 layout 跟提到的修正方案 :
.panBox{
// 改成 flex,不用再測距離
display: flex;
width : 100px;
height: 40px;
border: 1px solid #aaa;
justify-content: flex-start;
border-radius: 30px;
box-shadow: 0 3px 5px rgba(0,0,0,.3),
}
// 當 theme 變化而觸發
.panBox[data-theme=false]{
justify-content: flex-end;
}
.panThumb{
//display: inline-block; 有 flex 之後就不用額外設定 inline-block 屬性
width: 40px;
height: 40px;
cursor: pointer;
border-radius: 50%;
}
// JS
<motion.div
className="panBox"
data-theme={theme}
layout // 有用到 flex 排版,使用 layout 補強
animate={{
background: theme ? "#ABD9FF" : "#182747",
}}
>
<motion.span
className="panThumb"
layout
animate={{
background: theme ? "#fa0" : "#182747",
rotate: theme ? 0 : -160,
/* 改成動畫值 */
boxShadow: theme
? "inset 15px 8px rgba(255, 170, 0,0)"
: "inset 15px 8px rgba(255, 170, 0,1)",
}}
onPan={(e, info) => {
if (info.offset.x < 0) {
setTheme(true);
}
if (info.offset.x > 0) {
setTheme(false);
}
}}
/>
</motion.div>
或者另一種還我漂漂拳,打回 style ,效果也是一樣的
style={{
boxShadow: theme
? "inset 15px 8px rgba(255, 170, 0,0)"
: "inset 15px 8px rgba(255, 170, 0,1)",
}}
一天又平安的滑過去了,感謝 layout 的努力
初期常常寫出不知所以然的 CSS style,不知道什麼原因導致動畫不能動 QQ,曾經對著 span
操作 transform ,結果 span
一動也不動,這是因為 inline 屬性並沒有 transform 可以操作,像是 block
或 inline-
的屬性就可以。
參考 : CSS Transforms Module Level 1
A transformable element is an element in one of these categories:
- all elements whose layout is governed by the CSS box model except for non-replaced inline boxes, table-column boxes, and table-column-group boxes [CSS2],
layout
props 幫我們優化操作布局的元素,但也絕非萬能的,目前已存在的問題包含 :
下一篇會延續 layout 剩下的部分,包含 LayoutGroup 與 layout 在不同地方上的應用。